package org.msb.accentis.gui.models;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.event.TableModelEvent;
import javax.swing.table.AbstractTableModel;

import org.msb.accentis.data.DataManagerFactory;
import org.msb.accentis.data.Database;
import org.msb.accentis.data.IDataManager;
import org.msb.accentis.data.IDataManagerFactoryListener;
import org.msb.accentis.data.IDataManagerListener;
import org.msb.accentis.data.QueryParameterMap;
import org.msb.accentis.data.pojo.Account;
import org.msb.accentis.data.pojo.Transaction;

public class TransactionTableModel extends AbstractTableModel implements IDataManagerFactoryListener {

	private List<Transaction> list = new ArrayList<Transaction>();

	private List<BigDecimal> balances = new ArrayList<BigDecimal>();

	protected Account account;

	private final Map<String, Object> queryParameters = new HashMap<String, Object>();

	protected final IDataManagerListener<Transaction> dataManagerListener = new IDataManagerListener<Transaction>() {
		@Override
		public void objectCreated(Transaction object) {
			int index = -Collections.binarySearch(list, object) - 1;
			if (0 > index)
				return;

			list.add(index, object);

			BigDecimal previousBalance = getBalanceAt(index - 1);
			balances.add(index, previousBalance.add(object.getValueForAccount(getAccount())));

			fireTableRowsInserted(index, index);

			if (index < getRowCount() - 1 && 0 != object.getValueForAccount(getAccount()).compareTo(BigDecimal.ZERO)) {
				calculateBalances(index + 1, getRowCount() - 1);
				fireTableChanged(new TableModelEvent(TransactionTableModel.this, index + 1, getRowCount() - 1, 4));
			}
		}

		@Override
		public void objectRemoved(Transaction object) {
			int index = Collections.binarySearch(list, object);
			if (0 > index)
				return;

			list.remove(index);
			balances.remove(index);
			fireTableRowsDeleted(index, index);

			if (index < getRowCount() - 1) {
				calculateBalances(index, getRowCount() - 1);
				fireTableChanged(new TableModelEvent(TransactionTableModel.this, index, getRowCount() - 1, 4));
			}
		}

		@Override
		public void objectSaved(Transaction object) {
			/*
			 * When a Transaction has been saved, its date, which is the sort key, made have been modified, so we need to determine if the row corresponding to
			 * it has moved. In that case, the balances between the original position and the new position need to be recalculated.
			 * 
			 * Its amount could also have changed. In that case, the balances between the original position and the new position as well as every balances after
			 * that range need to be recalculated.
			 * 
			 * Determining if the position of the Transaction needs to change is done by recording the current position (original position), resorting the list,
			 * and then comparing the recorded original position to the new position. If they are different, the row has moved.
			 * 
			 * Determining if the amount has changed is done by calculating the sum of the value of the Transaction and the balance of the previous Transaction
			 * and comparing the result to the balance of the current Transaction. If they are different, the amount has changed.
			 */
			int preSortIndex = list.indexOf(object);
			Collections.sort(list);
			int postSortIndex = Collections.binarySearch(list, object);

			boolean amountChanged = hasAmountChanged(object, preSortIndex);
			boolean transactionMoved = preSortIndex != postSortIndex;

			int startIndex = Math.min(preSortIndex, postSortIndex);
			int endIndex = Math.max(preSortIndex, postSortIndex);

			if (amountChanged)
				endIndex = getRowCount();

			calculateBalances(startIndex, endIndex - 1);

			if (transactionMoved) {
				fireTableRowsDeleted(preSortIndex, preSortIndex);
				fireTableRowsInserted(postSortIndex, postSortIndex);
			}

			if (startIndex < endIndex)
				fireTableChanged(new TableModelEvent(TransactionTableModel.this, startIndex, endIndex - 1, 4));
		}

		@Override
		public void objectRefreshed(Transaction object) {
		}

		private boolean hasAmountChanged(Transaction object, int index) {
			BigDecimal balance = getBalanceAt(index - 1);
			balance = balance.add(object.getValueForAccount(getAccount()));

			return 0 != balance.compareTo(getBalanceAt(index));
		}
	};

	public TransactionTableModel() {
		DataManagerFactory.getInstance().addDataManagerFactoryListener(this);
	}

	public Account getAccount() {
		return account;
	}

	public void setAccount(Account value) {
		account = value;
		queryParameters.put("account", value); //$NON-NLS-1$

		reload();
	}

	protected List<Transaction> getTransactions() {
		return list;
	}
	
	protected List<BigDecimal> getBalances() {
		return balances;
	}
	
	public void reload() {
		clear();
		load(DataManagerFactory.getInstance().getDatabase().getDataManager());
	}

	private void clear() {
		int size = getRowCount();
		list.clear();
		balances.clear();
		fireTableRowsDeleted(0, size);
	}

	@Override
	public void databaseChanged(Database database) {
		clear();
		load(database.getDataManager());
	}

	private void load(IDataManager dataManager) {
		if (null != dataManager && null != account) {
			list.clear();
			balances.clear();

			List<Transaction> transactions = dataManager.<Transaction> runNamedQuery("Transaction.getTransactionDetailsForAccount", queryParameters); //$NON-NLS-1$
			for (int i = 0; i < transactions.size(); ++i) {
				Transaction transaction = transactions.get(i);
				list.add(transaction);
				balances.add(transaction.getValueForAccount(getAccount()).add(getBalanceAt(i - 1)));
			}

			dataManager.addDataManagerListener(dataManagerListener, Transaction.class);
			fireTableRowsInserted(0, getRowCount());
		}
	}

	public Transaction getTransactionAt(int index) {
		return list.get(index);
	}

	private BigDecimal getBalanceAt(int index) {
		if (-1 == index)
			return getAccount().getStartingBalance();

		return balances.get(index);
	}

	protected Map<String, Object> getPojoQueryParameters() {
		return QueryParameterMap.createSingleParameterMap("account", account); //$NON-NLS-1$
	}

	private void calculateBalances(int fromIndex, int toIndex) {
		if (toIndex < fromIndex)
			return;

		balances.subList(fromIndex, toIndex).clear();

		BigDecimal balance = getBalanceAt(fromIndex - 1);
		List<BigDecimal> insertedBalances = new ArrayList<BigDecimal>();

		for (int index = fromIndex; index <= toIndex; ++index) {
			balance = balance.add(list.get(index).getValueForAccount(getAccount()));
			insertedBalances.add(balance);
		}

		balances.addAll(fromIndex, insertedBalances);
	}

	@Override
	public int getRowCount() {
		return list.size();
	}

	@Override
	public int getColumnCount() {
		return 5;
	}

	@Override
	public Object getValueAt(int rowIndex, int columnIndex) {
		if (4 == columnIndex)
			return balances.get(rowIndex);

		Transaction transaction = getTransactionAt(rowIndex);
		switch (columnIndex) {
		case 0:
			return transaction.getDate();
		case 1:
			return transaction.getReference();
		case 2:
			return transaction.getPayee();
		case 3:
			return transaction.getValueForAccount(getAccount());
		}

		return null;
	}
}
